package info.motteke.util;

import static info.motteke.lang.StringUtil.*;
import static java.lang.String.*;
import info.motteke.lang.StringUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

/**
 * プロパティを保持するクラスです。
 *
 * @author YWak
 *
 */
public class Properties {

    /**
     * プロパティを保持するためのマップです。
     */
    private final Map<String, String> prop = getDefaultMap();

    /**
     * デフォルトの値を保持するプロパティです。
     */
    private final Properties defaultProp;

    /**
     * コンストラクタです。
     * 空のプロパティを作成します。
     */
    public Properties() {
        this(null);
    }

    /**
     * コンストラクタです。
     * デフォルトの値を持つプロパティを引数に保持します。
     * このプロパティは、
     *
     * @param def
     *            デフォルトの値を持つプロパティ
     */
    public Properties(Properties def) {
        defaultProp = def;
        putAll(def);
    }

    /**
     * プロパティを取得します。<br /> {@code key != null}であるkeyに対応したプロパティを返します。<br />
     * keyに対応したプロパティが設定されていない場合には、デフォルトの値を検索します。<br />
     * 最終的にプロパティが見つからない場合にnullを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @return keyに対応したプロパティ。ただし見つからなければnull。
     */
    public String getProperty(String key) {
        if (key == null) {
            throw new NullPointerException("キーはnullにできません。");
        }
        String value = prop.get(key);
        boolean useDefault = (value == null) && hasDefault();
        return useDefault ? defaultProp.getProperty(key) : value;
    }

    /**
     * プロパティを取得します。<br />
     * ただし、{@code getProperty(key) == null}であれば、defを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param def
     *            プロパティが見つからない場合のデフォルト値
     * @return keyに対応したプロパティ。ただし見つからなければdef。
     */
    public String getProperty(String key, String def) {
        String value = getProperty(key);
        return value == null ? def : value;
    }

    /**
     * プロパティをbooleanに変換して返します。 <br />
     * 変換ルールは{@linkplain StringUtil#parseBoolean(String)}に準拠します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @return keyに対応したプロパティ。
     */
    public boolean getBooleanProperty(String key) {
        return parseBoolean(getProperty(key));
    }

    /**
     * プロパティをbyteに変換して返します。 <br />
     * ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param def
     *            デフォルト値
     * @return keyに対応したプロパティ。<br />
     *         ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     */
    public byte getByteProperty(String key, byte def) {
        return parseByte(getProperty(key), def);
    }

    /**
     * プロパティの一文字目を返します。 <br />
     * ただし、対応するプロパティが見つからない場合はdefを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param def
     *            デフォルト値
     * @return keyに対応したプロパティの一文字目。<br />
     *         ただし、対応するプロパティが見つからない場合はdefを返します。
     */
    public char getCharProperty(String key, char def) {
        String value = getProperty(key);

        return StringUtil.isEmpty(value) ? def : value.charAt(0);
    }

    /**
     * プロパティをshortに変換して返します。 <br />
     * ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param def
     *            デフォルト値
     * @return keyに対応したプロパティ。<br />
     *         ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     */
    public short getShortProperty(String key, short def) {
        return parseShort(getProperty(key), def);
    }

    /**
     * プロパティをintに変換して返します。 <br />
     * ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param def
     *            デフォルト値
     * @return keyに対応したプロパティ。<br />
     *         ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     */
    public int getIntProperty(String key, int def) {
        return parseInt(getProperty(key), def);
    }

    /**
     * プロパティをlongに変換して返します。 <br />
     * ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param def
     *            デフォルト値
     * @return keyに対応したプロパティ。<br />
     *         ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     */
    public long getLongProperty(String key, long def) {
        return parseLong(getProperty(key), def);
    }

    /**
     * プロパティをfloatに変換して返します。 <br />
     * ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param def
     *            デフォルト値
     * @return keyに対応したプロパティ。<br />
     *         ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     */
    public float getFloatProperty(String key, float def) {
        return parseFloat(getProperty(key), def);
    }

    /**
     * プロパティをdoubleに変換して返します。 <br />
     * ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param def
     *            デフォルト値
     * @return keyに対応したプロパティ。<br />
     *         ただし、対応するプロパティが見つからない場合や変換できない場合はdefを返します。
     */
    public double getDoubleProperty(String key, double def) {
        return parseDouble(getProperty(key), def);
    }

    /**
     * プロパティを設定します。<br />
     * keyはnullに出来ません。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, String value) {
        if (key == null) {
            throw new NullPointerException("キーはnullに出来ません。");
        }
        return prop.put(key, value);
    }

    /**
     * プロパティを設定します。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, boolean value) {
        return setProperty(key, valueOf(value));
    }

    /**
     * プロパティを設定します。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, byte value) {
        return setProperty(key, valueOf(value));
    }

    /**
     * プロパティを設定します。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, char value) {
        return setProperty(key, valueOf(value));
    }

    /**
     * プロパティを設定します。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, short value) {
        return setProperty(key, valueOf(value));
    }

    /**
     * プロパティを設定します。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, int value) {
        return setProperty(key, valueOf(value));
    }

    /**
     * プロパティを設定します。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, long value) {
        return setProperty(key, valueOf(value));
    }

    /**
     * プロパティを設定します。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, float value) {
        return setProperty(key, valueOf(value));
    }

    /**
     * プロパティを設定します。<br />
     * 既にプロパティが設定されている場合は、上書きされます。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @param value
     *            keyに対応したプロパティ
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String setProperty(String key, double value) {
        return setProperty(key, valueOf(value));
    }

    /**
     * プロパティを削除します。<br />
     * ただし、コンストラクタで与えたデフォルト値は削除されません。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @return 前に設定されていたプロパティ。ただし設定されていなかった場合はnull。
     */
    public String removeProperty(String key) {
        String old = prop.remove(key);

        if(hasDefault()){
            prop.put(key, defaultProp.getProperty(key));
        }
        return old;
    }

    /**
     * プロパティを全て削除します。
     * ただし、コンストラクタで与えたデフォルト値は削除されません。
     */
    public void clear() {
        prop.clear();
    }

    /**
     * キーに該当するプロパティが存在するかを判断します。
     *
     * @param key
     *            プロパティを一意に表す文字列
     * @return プロパティが存在すればtrue、さもなくばfalse
     */
    public boolean containsKey(String key) {
        if (prop.containsKey(key)) {
            return true;
        }
        if (hasDefault()) {
            return defaultProp.containsKey(key);
        } else {
            return false;
        }

    }

    /**
     * このプロパティが含まれているかを判断します。
     *
     * @param value
     *            プロパティを表す文字列
     * @return プロパティとして値が存在すればtrue、さもなくばfalse
     */
    public boolean containsValue(String value) {
        if (prop.containsValue(value)) {
            return true;
        }
        if (hasDefault()) {
            return defaultProp.containsValue(value);
        } else {
            return false;
        }

    }

    /**
     * プロパティのキーと値の対応を返します。
     *
     * @return キーと値のペアを格納したセット
     */
    public Set<Entry<String, String>> entrySet() {
        return prop.entrySet();
    }

    /**
     * プロパティが空であるかを判断します。
     *
     * @return プロパティが空であればtrue、さもなくばfalse。
     */
    public boolean isEmpty() {
        return this.size() == 0;
    }

    /**
     * プロパティを表すキーの集合を返します。
     *
     * @return キーの集合
     */
    public Set<String> keySet() {
        return prop.keySet();
    }

    /**
     * マップに格納されている値をプロパティに格納します。
     *
     * @param map プロパティを格納したマップ
     */
    public void putAll(Map<String, String> map) {
        prop.putAll(map);
    }

    /**
     * プロパティを全てコピーします。
     *
     * @param prop コピー元のプロパティ
     */
    public void putAll(Properties prop){
        if(prop == null){
            return;
        }
        for(Entry<String, String> entry : prop.entrySet()){
            this.prop.put(entry.getKey(), entry.getValue());
        }
    }

    /**
     * 保持しているプロパティの数を返します。
     *
     * @return 保持しているプロパティの数
     */
    public int size() {
        return prop.size();
    }

    /**
     * 保持しているプロパティを格納したコレクションを返します。
     *
     * @return 保持しているプロパティを格納したコレクション
     */
    public Collection<String> values() {
        return prop.values();
    }

    /**
     * {@linkplain InputStream}からプロパティを読み出し、格納します。<br />
     * 書式は{@linkplain java.util.Properties#load(InputStream)}に準拠しています。<br />
     * 利用される文字コードはシステム標準の文字コードです。
     *
     * @param is 入力ストリーム
     * @return 自分自身
     * @throws IOException 入出力例外が発生した場合
     * @see java.util.Properties#load(InputStream)
     */
    public Properties load(InputStream is) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.load(is);
        copy(p);
        return this;
    }

    /**
     * {@linkplain Reader}からプロパティを読み出し、格納します。
     * 書式は{@linkplain java.util.Properties#load(Reader)}に準拠しています。
     *
     * @param reader 入力リーダ
     * @return 自分自身
     * @throws IOException 入出力例外が発生した場合
     * @see java.util.Properties#load(Reader)
     */
    public Properties load(Reader reader) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.load(reader);
        copy(p);
        return this;
    }

    /**
     * {@linkplain InputStream}からプロパティを読み出し、格納します。<br />
     * 書式は{@linkplain java.util.Properties#loadFromXML(InputStream)}に準拠しています。<br />
     *
     * @param is 入力ストリーム
     * @return 自分自身
     * @throws IOException 入出力例外が発生した場合
     * @see java.util.Properties#load(InputStream)
     */
    public Properties loadFromXML(InputStream is) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.loadFromXML(is);
        copy(p);
        return this;
    }

    /**
     * {@linkplain OutputStream}にプロパティを書き出します。
     *
     * @param os
     * @param comment
     * @return
     * @throws IOException
     */
    public Properties store(OutputStream os, String comment) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.putAll(prop);
        p.store(os, comment);
        return this;
    }

    public Properties store(Writer writer, String comment) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.putAll(prop);
        p.store(writer, comment);
        return this;
    }

    public Properties storeToXML(OutputStream os, String comment) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.putAll(prop);
        p.storeToXML(os, comment);
        return this;
    }

    public Properties storeToXML(OutputStream os, String comment, String encode) throws IOException {
        java.util.Properties p = new java.util.Properties();
        p.putAll(prop);
        p.storeToXML(os, comment, encode);
        return this;
    }

    public Map<String, String> asMap() {
        return Collections.unmodifiableMap(prop);
    }

    public java.util.Properties asProperties() {
        java.util.Properties p = new java.util.Properties();
        p.putAll(prop);

        return p;
    }

    protected Map<String, String> getDefaultMap() {
        return new HashMap<String, String>();
    }

    private boolean hasDefault(){
        return defaultProp != null;
    }

    private void copy(java.util.Properties p) {
        Enumeration<Object> e = p.elements();

        while (e.hasMoreElements()) {
            String key = (String) e.nextElement();
            setProperty(key, p.getProperty(key));
        }
    }
}
